﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Reflection;
using System.Linq.Expressions;

namespace AirLinq.Meta {
	public static class XmlModelReader {
		public static bool IsNamespace(XmlReader reader) {
			return reader.LookupNamespace(reader.Prefix) == "http://code.goole.com/q/airlinq/mapping/2011";
		}
		public static MetaModel ReadMapping(XmlReader reader) {
			reader.MoveToContent();
			if (!IsNamespace(reader) || reader.LocalName != "air-model") {
				return null;
			}
			var model = new MetaModel();
			if (reader.IsEmptyElement) {
				reader.Skip();
				return model;
			}
			reader.ReadStartElement();
			reader.MoveToContent();
			while (reader.NodeType != XmlNodeType.EndElement) {
				if (reader.NodeType == XmlNodeType.Whitespace || !IsNamespace(reader)) {
					reader.Skip();
					continue;
				}
				switch (reader.LocalName) {
					case "type":
						model.MetaTypes.Add(ReadMetaType(reader));
						break;
					default:
						throw new XmlSchemaException("unsupported tag: " + reader.LocalName);
				}
				reader.MoveToContent();
			}
			reader.ReadEndElement();
			return model;
		}
		private static MetaTypeTemplate ReadMetaType(XmlReader reader) {
			if (!IsNamespace(reader) || (reader.LocalName != "type")) {
				throw new XmlSchemaException("expected element 'type'");
			}
			var className = ReadRequiredAttribute(reader, "class");
			var tableName=ReadRequiredAttribute(reader,"table");
			var classType = Type.GetType(className);
			if (classType == null) {
				throw new XmlSchemaException("class not found");
			}
			var members = new List<MetaMember>();
			var associations=new List<MetaAssociation>();
			if (!reader.IsEmptyElement) {
				reader.ReadStartElement();
				reader.MoveToContent();
				while (reader.NodeType != XmlNodeType.EndElement) {
					if (reader.NodeType == XmlNodeType.Whitespace || !IsNamespace(reader)) {
						reader.Skip();
					} else {
						switch (reader.LocalName) {
							case "property":
							case "field":
								members.Add(ReadMetaMember(classType,reader));
								break;
							case "association":
								associations.Add(ReadMetaAssociation(reader));
								break;
							default:
								throw new XmlSchemaException("unsupported tag: " + reader.LocalName);
						}
					}
					reader.MoveToContent();
				}
				reader.ReadEndElement();
				return new MetaType(classType, members.AsReadOnly(), associations.AsReadOnly(), tableName);
			}
			throw new XmlSchemaException("empty tag: type");
		} 
		private static MetaAssociation ReadMetaAssociation(XmlReader reader) {
			if (!IsNamespace(reader) || reader.LocalName != "association") {
				throw new XmlSchemaException("expected element 'association'");
			}
			throw new NotImplementedException();
			AssertEmptyElement(reader);
			return new MetaAssociation();
		}
		private static void AssertEmptyElement(XmlReader reader) {
			if (!reader.IsEmptyElement) {
				string name = reader.Name;
				reader.Read();
				if (reader.NodeType != XmlNodeType.EndElement) {
					throw new  XmlSchemaException("must be empty element");
				}
			}
			reader.Skip();
		}
		private static MetaMember ReadMetaMember(Type type, XmlReader reader) {
			if (!IsNamespace(reader) || (reader.LocalName != "field" && reader.LocalName!="property")) {
				throw new XmlSchemaException("expected element 'field'");
			}
			var name = ReadRequiredAttribute(reader, "name");
			var column = ReadOptionalAttribute(reader, "column");
			var expressionStr = ReadOptionalAttribute(reader, "expression");
			if (string.IsNullOrEmpty(column) && string.IsNullOrEmpty(expressionStr)
				|| !string.IsNullOrEmpty(column) && !string.IsNullOrEmpty(expressionStr)) {
				throw new XmlSchemaException("expected one of attribute 'column' or 'expression'");
			}
			var primaryKeyStr=ReadOptionalAttribute(reader,"primaryKey");
			var primaryKey=!(string.IsNullOrEmpty(primaryKeyStr) || primaryKeyStr.ToLowerInvariant() == "false");
			LambdaExpression le=null;
			if (!string.IsNullOrEmpty(expressionStr)){
				//TODO Compile expresion;
			}
			switch (reader.LocalName){
				case "field":
					if (!type.IsClass) {
						throw new XmlSchemaException("fields supports only for classes");
					}
					var field = type.GetField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
					if (field == null) {
						throw new XmlSchemaException("field not found");
					}
					AssertEmptyElement(reader);
					return new MetaField(field, column, le, primaryKey);
				case "property":
					var property=type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
					if (property == null) {
						throw new XmlSchemaException("property not found");
					}
					var storage=ReadOptionalAttribute(reader, "storage");
					if(!string.IsNullOrEmpty(storage) && !type.IsClass){
						throw new XmlSchemaException("attribute 'storage' allowed for class");
					}
					if (!type.IsInterface && string.IsNullOrEmpty(storage) && !property.CanWrite){
						throw new XmlSchemaException("for non-interface type property must have 'set' accessor or have 'storage' attribute");
					}
					FieldInfo storageField=null;
					if (!string.IsNullOrEmpty(storage)){
						storageField = type.GetField(storage, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
						if (storage == null) {
							throw new XmlSchemaException("field not found");
						}
					}
					AssertEmptyElement(reader);
					return new MetaProperty(property, column, le, primaryKey, storageField);
				default:
					throw new XmlSchemaException("expected one of tags: 'field', 'property'");
			}
		}

		private static string ReadOptionalAttribute(XmlReader reader, string attribute) {
			return reader.GetAttribute(attribute);
		}
		private static string ReadRequiredAttribute(XmlReader reader, string attribute) {
			var res = ReadOptionalAttribute(reader, attribute);
			if (string.IsNullOrEmpty(res)) {
				throw new XmlSchemaException("expected non empty attribute: " + attribute);
			}
			return res;
		}
	}
}
